//
// Include changes:
//  - Jan 12, 2014 Updated for Delphi XE5 by francois.piette@overbyte.be
//    Merged by David Millington for XE4

unit Tiles;

interface

uses
  FMX.Types, FMX.PixelFormats,
  System.UITypes, SysUtils, System.Types;

const
  // DWS Mandelbrot tiles are always this big
  TILE_WIDTH = 256;
  TILE_HEIGHT = 256;

type

  TTileData = array of Integer;

  // Created in Created state (probably queued for downloading, etc.) Once data downloaded or failed,
  // it needs to be processed to create the bitmap, in the main thread. Once that is done, it's drawable.
  TTileState = (tsCreated, tsGenerateBitmap, tsDrawable);

  TTile = class
  private
    FZoomLevel,
    FX,
    FY : Integer;

    FState : TTileState;
    FBitmap : TBitmap;
    FData : TTileData;
    FError : string;

    function GetCanDraw: Boolean;
    function GetNeedToProcessBitmap: Boolean;
  public
    constructor Create(const ZoomLevel, X, Y : Integer);
    destructor Destroy; override;

    // Called from the download thread
    procedure InitFromThreadWithData(const Data : TTileData);
    procedure InitFromThreadError(const E : Exception; const URL : string);

    procedure GenerateBitmap;
    procedure Draw(Bitmap : TBitmap; DestRect : TRectF);

    function Matches(const Zoom, X, Y : Integer) : Boolean;
    function IsOutOfBounds : Boolean;
    class function OutOfBounds(const Zoom, X, Y : Integer) : Boolean;

    function GetApproxMemBytes : Integer;

    property Zoom : Integer read FZoomLevel;
    property X : Integer read FX;
    property Y : Integer read FY;
    property NeedToProcessBitmap : Boolean read GetNeedToProcessBitmap;
    property CanDraw : Boolean read GetCanDraw;
  end;

implementation

uses
  ColorMapping, Math;

{ TTile }

constructor TTile.Create(const ZoomLevel, X, Y: Integer);
begin
  inherited Create;

  FBitmap := nil;
  SetLength(FData, 0);
  FState := tsCreated;

  FZoomLevel := ZoomLevel;
  FX := X;
  FY := Y;
end;

destructor TTile.Destroy;
begin
  FBitmap.Free;
  SetLength(FData, 0);

  inherited;
end;

procedure TTile.InitFromThreadWithData(const Data: TTileData);
begin
  // Called by the download thread. Copy the data. Converting to a bitmap has
  // to happen in the main thread, so flag the tile is ready for mainthread
  // processing
  FData := Copy(Data, Low(Data), Length(Data));
  FState := tsGenerateBitmap;
end;

procedure TTile.InitFromThreadError(const E: Exception; const URL: string);
begin
  // Called from the download thread: it encountered an error
  FError := 'Download error: "' + E.Message + '" accessing URL "' + URL + '"';
  FState := tsGenerateBitmap; // Draw the above error message on the tile
end;

procedure TTile.GenerateBitmap;
var
  Color : TAlphaColor;
  X, Y : Integer;
  Row  : PAlphaColor;
  BitmapData : TBitmapData;
  PixelFormat : TPixelFormat;
begin
  assert(FState = tsGenerateBitmap);

  FBitmap.Free;
  FBitmap := TBitmap.Create(TILE_WIDTH, TILE_HEIGHT);
  FBitmap.Canvas; // Force the canvas to be created, stays nil otherwise!

  FBitmap.Canvas.BeginScene;
  try
    // If there was an error, draw it
    if FError <> '' then begin
      FBitmap.Clear(TAlphaColors.Black);
      FBitmap.Canvas.FillText(RectF(0, 0, TILE_WIDTH, TILE_HEIGHT),
        FError, True, 1.0, [], TTextAlign.taCenter, TTextAlign.taCenter);
    end else if (Length(FData) = 1) then begin
      // An array of one element means the tile is a single solid colour
      Color := ColorForValue(FData[0]);
      FBitmap.Clear(Color);
    end else if (Length(FData) = TILE_WIDTH * TILE_HEIGHT) then begin
      // Array has elements for every pixel
      FBitmap.Map(TMapAccess.maWrite, BitmapData);
      try
        PixelFormat := FBitmap.PixelFormat;
        for Y := 0 to TILE_HEIGHT-1 do begin
          Row := PAlphaColor(PByte(BitmapData.Data) + (Y * BitmapData.Pitch));
          for X := 0 to TILE_WIDTH-1 do begin
            Color := ColorForValue(FData[TILE_WIDTH * Y + X]);
            AlphaColorToPixel(Color, Row, PixelFormat);
            Inc(Row);
          end;
        end;
      finally
        FBitmap.Unmap(BitmapData);
      end;
    end else begin
      // Something odd about the returned data
      FBitmap.Clear(TAlphaColors.Black);
    end;
  finally
    FBitmap.Canvas.EndScene;
  end;

  FState := tsDrawable;
end;

procedure TTile.Draw(Bitmap: TBitmap; DestRect : TRectF);
begin
  if FState <> tsDrawable then Exit; // Probably downloading still
  assert(Assigned(FBitmap));

  // At some fractional zoom levels, the edges of tiles are visible. Offsetting the whole rect by 0.5
  // doesn't solve this, but drawing the rect to 0.5 pixels bigger than it is does.
  DestRect.Width := DestRect.Width + 0.5;
  DestRect.Height := DestRect.Height + 0.5;

  Bitmap.Canvas.BeginScene;
  try
    Bitmap.Canvas.DrawBitmap(FBitmap, RectF(0, 0, TILE_WIDTH, TILE_HEIGHT), DestRect, 1.0);

    {$ifdef DEBUG}
      // Draw the tile ID in the center of the tile
      Bitmap.Canvas.FillText(DestRect,
        'z' + IntToStr(FZoomLevel) + 'x' + IntToStr(FX) + 'y' + IntToStr(FY),
        True, 1.0, [], TTextAlign.taCenter, TTextAlign.taCenter);
    {$endif}
  finally
    Bitmap.Canvas.EndScene;
  end;
end;

function TTile.Matches(const Zoom, X, Y: Integer): Boolean;
begin
  Result := (FZoomLevel = Zoom) and (FX = X) and (FY = Y);
end;

class function TTile.OutOfBounds(const Zoom, X, Y: Integer): Boolean;
var
  MaxTiles : Integer;
begin
  // The server only has the exact tiles for each zoom level, but flexible scrolling etc
  // lets us have tiles that aren't inside the set area. For zoom z, there are 2^z tiles.
  MaxTiles := Round(Power(2.0, Zoom));
  if (X < 0) or (X >= MaxTiles) then Exit(True);
  if (Y < 0) or (Y >= MaxTiles) then Exit(True);
  Exit(False);
end;

function TTile.IsOutOfBounds: Boolean;
begin
  Result := TTile.OutOfBounds(FZoomLevel, FX, FY);
end;

function TTile.GetApproxMemBytes: Integer;
begin
  // Estimate of memory usage.  Count the data array size and the bitmap, assuming it is 32bpp
  Result := Length(FData) * SizeOf(Integer);
  if Assigned(FBitmap) then
    Inc(Result, TILE_WIDTH * TILE_HEIGHT * 4); // 4 bytes per pixel
end;

function TTile.GetCanDraw: Boolean;
begin
  Result := FState = tsDrawable;
end;

function TTile.GetNeedToProcessBitmap: Boolean;
begin
  Result := FState = tsGenerateBitmap;
end;

end.
